package erfgame.core.world.terrain;

import java.util.logging.Level;
import java.util.logging.Logger;

import erfgame.core.Point3D;

public abstract class AbstractVoxelColorPicker {
	
	private static final Logger log = Logger.getLogger( AbstractVoxelColorPicker.class.getName() );

	private static final int SWAPPED_FLAG = 0x010000;
	
	public static final int PRECISION = 12;
	public static final int PRECISION_PLUS_1 = PRECISION + 1;
	public static final int ONE = 1 << PRECISION;
	public static final int INVALID = Integer.MIN_VALUE;
	

//	public static final Point3D[] DIAG_Z_LEFT_HANDED = new Point3D[]{new Point3D(-1, -1, 0),new Point3D(0, 0, 1),new Point3D(1, 1, 0),new Point3D(0, 0, -1)};
//	public static final Point3D[] DIAG_Z_RIGHT_HANDED = new Point3D[]{new Point3D(-1, -1, 0),new Point3D(0, 0, -1),new Point3D(1, 1, 0),new Point3D(0, 0, 1)};

	public static final Point3D[] YZ_LEFT_HANDED = new Point3D[]{new Point3D(0, -1, 0),new Point3D(0, 0, 1),new Point3D(0, 1, 0),new Point3D(0, 0, -1)};
	public static final Point3D[] YZ_RIGHT_HANDED = new Point3D[]{new Point3D(0, -1, 0),new Point3D(0, 0, -1),new Point3D(0, 1, 0),new Point3D(0, 0, 1)};

	public static final Point3D[] XZ_LEFT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 0, 1),new Point3D(1, 0, 0),new Point3D(0, 0, -1)};
	public static final Point3D[] XZ_RIGHT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 0, -1),new Point3D(1, 0, 0),new Point3D(0, 0, 1)};

	public static final Point3D[] XY_LEFT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, 1, 0),new Point3D(1, 0, 0),new Point3D(0, -1, 0)};
	public static final Point3D[] XY_RIGHT_HANDED = new Point3D[]{new Point3D(-1, 0, 0),new Point3D(0, -1, 0),new Point3D(1, 0, 0),new Point3D(0, 1, 0)};

	public static final int ROTATION_LENGTH = 4;
	public static final int ROTATION_LENGTH_TIMES_1_PT_5 = ROTATION_LENGTH + ROTATION_LENGTH/2;
	public static final int ROTATION_LENGTH_PLUS_1 = ROTATION_LENGTH+1;
	public static final int ROTATION_LENGTH_MINUS_1 = ROTATION_LENGTH-1;
	public static final int ROTATION_MASK = ROTATION_LENGTH_MINUS_1;
	
	public static final int[][] generateAtanValues( double lightAngle, int smoothness, boolean abs ) {
		int smoothnessExtent = smoothness * 2;
		int[][] verticalAtanValues = new int[smoothnessExtent*2+1][smoothnessExtent*2+1];

		int bottom = smoothnessExtent;
		for( int bottomIndex = smoothnessExtent*2; bottomIndex>=0; bottomIndex-- ) {
			int[] atan2ValuesBottom = verticalAtanValues[bottomIndex]; 
			int top = smoothnessExtent;
			for( int topIndex = smoothnessExtent*2; topIndex>=0; topIndex-- ) {
				if(top != 0 || bottom != 0) {
					double atan2 = Math.atan2(top, bottom);
					int precisionMultiplier = ONE; 
					double a = Math.sin(atan2+lightAngle) * precisionMultiplier;
					if( abs ) {
						a = Math.abs( a );
					}
					atan2ValuesBottom[topIndex] = (int)Math.round(a);
				}
				top--;
			}
			bottom--;
		}

		return verticalAtanValues;
	}
	
	protected int smoothness;
	protected int smoothnessMinus1;
	protected int smoothnessTimes2;
	
	protected int blockWidth;
	protected int blockHeight;
	protected int blockDepth;
	
	protected boolean useLog;
	
	public AbstractVoxelColorPicker( int smoothness,  int blockWidth, int blockHeight, int blockDepth, boolean useLog ) {
		this.smoothness = smoothness;
		this.smoothnessMinus1 = smoothness - 1;
		this.smoothnessTimes2 = smoothness * 2;
		
		this.blockWidth = blockWidth;
		this.blockHeight = blockHeight;
		this.blockDepth = blockDepth;
		
		this.useLog = useLog;
		
	}
	
	public abstract int getColor( 
			TerrainColorSource colorSource,
			int cx, 
			int cy, 
			int cz,
			byte[][][][][][] terrain, 
			int tx, 
			int ty, 
			int tz, 
			int dCeiling, 
			int worldX, 
			int worldY, 
			int worldZ,
			boolean clearRoof
	);	

	public final boolean getPathDeltas( 
			int cx, int cy, int cz,
			byte[][][][][][] terrain, 
			int x, int y, int z, 
			Point3D[] leftRotation, Point3D[] rightRotation,
			boolean allowCurves,
			Point3D result, 
			VerticalEdge firstVerticalEdge,
			boolean clearRoof ) {
		int upResult = getPathDeltas( 
				cx, cy, cz, 
				terrain, 
				x, y, z, 
				rightRotation, 
				allowCurves, 
				result, 
				firstVerticalEdge,
				clearRoof
		);
		int upx = result.x;
		int upy = result.y;
		int upz = result.z;
		
		int downResult = getPathDeltas( 
				cx, cy, cz, 
				terrain, 
				x, y, z, 
				leftRotation, 
				allowCurves, 
				result, 
				firstVerticalEdge,
				clearRoof
		);

		int downx = result.x;
		int downy = result.y;
		int downz = result.z;		
		
		boolean valid;
		if( upResult > 0 && downResult > 0 ) {
			// don't normalize? 
			// need to make sure they are (kind of) aligned
			int upPrimary = ((upResult & 0xFF)-1);
			// they are opposites, so need to rotate one
			int downPrimary = (ROTATION_LENGTH_TIMES_1_PT_5-(downResult & 0xFF)+1) & ROTATION_MASK;
			if( upPrimary == downPrimary ) {
				result.x = upx - downx;
				result.y = upy - downy;
				result.z = upz - downz;
				valid = true;
			} else {
				boolean upSwapped = (SWAPPED_FLAG & upResult)!=0;
				boolean downSwapped = (SWAPPED_FLAG & downResult)!=0;
				// TODO could probablly use xor
				if( upSwapped && downSwapped || !upSwapped && !downSwapped || allowCurves ) {
					// use both
					result.x = upx - downx;
					result.y = upy - downy;
					result.z = upz - downz;
					valid = true;
				} else if( upSwapped ) {
					// use down
					result.x = -downx;
					result.y = -downy;
					result.z = -downz;					
					valid = true;
				} else /*if( downSwapped )*/ {
					// use up
					result.x = upx;
					result.y = upy;
					result.z = upz;
					valid = true;
				}
			}
		} else if( upResult > 0 ) {
			result.x = upx;
			result.y = upy;
			result.z = upz;
			valid = true;
		} else if( downResult > 0 ) {
			result.x = -downx;
			result.y = -downy;
			result.z = -downz;
			valid = true;
		} else {
			valid = false;
		}
		// TODO delete this
		if( useLog && log.isLoggable(Level.INFO) ) {
			log.info("----");
			log.info( "up result : "+Integer.toHexString(upResult) );
			log.info( "up delta : ("+upx+","+upy+","+upz+")" );
			log.info( "down result : "+Integer.toHexString(downResult) );
			log.info( "down delta : ("+downx+","+downy+","+downz+")" );
			log.info( "edge : "+firstVerticalEdge );
			log.info( "result : "+result );
			log.info( "valid : "+valid );
		}
		

		return valid;
	}
	
	public int getPathDeltas(
		int cx, int cy, int cz,
		byte[][][][][][] terrain, 
		int x, int y, int z, 
		Point3D[] offsetOrder,
		boolean allowCurves,
		Point3D deltas, 
		VerticalEdge edge,
		boolean clearRoof
	) 
	{
		int ax = x;
		int ay = y;
		int az = z;
		
		int previousTurnX = ax;
		int previousTurnY = ay;
		int previousTurnZ = az;
		
		int primaryIndex = -1;
		int secondaryIndex = -1;
		int previousIndex = -1;
		int deltaX = 0;
		int deltaY = 0;
		int deltaZ = 0;
		boolean done = false;
		byte[][][] cterrain = terrain[cx][cy][cz];
		int pathLength = smoothness; 
		int turn = 0;
		int edgeRemainingLength = 0;
		int edgecx = 0;
		int edgecy = 0;
		int edgecz = 0;
		int edgex = 0;
		int edgey = 0;
		int edgez = 0;
		boolean swappedIndex = false;

		outer: while( !done ) {
			pathLength--;
			if( pathLength > 0 ) {
				int previousConsecutiveAir = 0;
				for( int i=0; i<=offsetOrder.length; i++ ) {
					int index = (i+previousIndex+ROTATION_LENGTH_MINUS_1) & ROTATION_MASK;
					Point3D offset = offsetOrder[index];
					int dx = offset.x;
					int dy = offset.y;
					int dz = offset.z;
					byte[][][] ctterrain = cterrain;
					int ctx = cx;
					int cty = cy;
					int ctz = cz;
					int tx = x + dx;
					int ty = y + dy;
					int tz = z + dz;
					// move to the next block
					if( tx < 0 ) {
						if( ctx > 0 ) {
							ctx--;
							tx += blockWidth;
							ctterrain = terrain[ctx][cty][ctz];
						} else {
							ctterrain = null;
						}
					} else if( tx >= blockWidth ) {
						ctx++;
						if( ctx < terrain.length ) {
							tx = tx -= blockWidth;
							ctterrain = terrain[ctx][cty][ctz];
						} else {
							ctterrain = null;
						}
					}
					if( ctterrain != null ) {
						if( ty < 0 ) {
							if( cty > 0 ) {
								cty--;
								ty += blockHeight;
								ctterrain = terrain[ctx][cty][ctz];
							} else {
								ctterrain = null;
							}
						} else if( ty >= blockHeight ) {
							cty++;
							if( cty < terrain[ctx].length ) {
								ty = ty -= blockHeight;
								ctterrain = terrain[ctx][cty][ctz];
							} else {
								ctterrain = null;
							}
						}
						if( ctterrain != null ) {
							if( tz < 0 ) {
								if( ctz > 0 ) {
									ctz--;
									tz += blockDepth;
									ctterrain = terrain[ctx][cty][ctz];						
								} else {
									ctterrain = null;
								}
							} else if( tz >= blockDepth ) {
								ctz++;
								if( ctz < terrain[ctx][cty].length ) {
									tz = tz -= blockDepth;
									ctterrain = terrain[ctx][cty][ctz];
								} else {
									ctterrain = null;
								}
							}
						}
					}
					boolean valid = ctterrain != null || (clearRoof && az == 0);
					if( valid ) {
						boolean isSolid;
						if( ctterrain != null ) {
							byte terrainType = ctterrain[ tx ][ ty ][ tz ];
							isSolid = terrainType != TerrainConstants.TERRAIN_TYPE_AIR;
						} else {
							isSolid = false;
						}
						if( edge != null && !isSolid && dz==0 && edgeRemainingLength == 0 && edge.remainingLength < pathLength ) {
							if( primaryIndex >= 0 ) {
								// need to see if we hit another valid turn
								edgecx = cx;
								edgecy = cy;
								edgecz = cz;
								edgex = x;
								edgey = y;
								edgez = z;
								edgeRemainingLength = pathLength;
							} else {
								// our first line has a vertical edge, so we're good
								edge.cx = cx;
								edge.cy = cy;
								edge.cz = cz;
								edge.x = x;
								edge.y = y;
								edge.z = z;
								edge.remainingLength = pathLength;								
							}
						}
						if( previousConsecutiveAir > 0 && isSolid || isSolid && i==offsetOrder.length && previousIndex >= 0 ) {
							int atx = ax + dx;
							int aty = ay + dy;
							int atz = az + dz;
							if( previousIndex < 0 || index == previousIndex ) {
								if( primaryIndex < 0 ) {
									primaryIndex = index;
								} else if( primaryIndex != index ) {
									// drop out, we've hit a major turn (more than one pixel in depth against the current direction)
									if( secondaryIndex >= 0 && pathLength == smoothness-3 && index == secondaryIndex ) {
										// it's possible we've got the primary and secondary indexes mixed up if we started on a corner
										int tempIndex = secondaryIndex;
										secondaryIndex = primaryIndex;
										primaryIndex = tempIndex;
										swappedIndex = true;
									} else if( !allowCurves ) {
										break outer;
									}									
								}
								cx = ctx;
								cy = cty;
								cz = ctz;
								ax = atx;
								ay = aty;
								az = atz;
								previousIndex = index;
								x = tx;
								y = ty;
								z = tz;
								cterrain = ctterrain;
								continue outer;
							} else {
								if( !allowCurves && index == ((primaryIndex + 2) & ROTATION_MASK) ) {
									// reversed primary index, also time to exit
									break outer;
								} else if( secondaryIndex >= 0 && !allowCurves && index != primaryIndex && index != secondaryIndex ) {
									// turned in a direction we didn't expect
									break outer;
								} else if( secondaryIndex < 0 ) { 
									secondaryIndex = index;
								} else if( !allowCurves && secondaryIndex != index && primaryIndex != index ) {
									break outer;
								}
								// the edge is valid as we have a valid turn (and it's not this turn)
								if( edgeRemainingLength != 0 && edgeRemainingLength != pathLength ) {
									edge.cx = edgecx;
									edge.cy = edgecy;
									edge.cz = edgecz;
									edge.x = edgex;
									edge.y = edgey;
									edge.z = edgez;
									edge.remainingLength = edgeRemainingLength;
									edgeRemainingLength = 0;
								}
								// we've hit a turn, add the turn direction see if we've maxed out
								deltaX += (ax - previousTurnX);
								deltaY += (ay - previousTurnY);
								deltaZ += (az - previousTurnZ);
								previousTurnX = ax;
								previousTurnY = ay;
								previousTurnZ = az;
								turn++;
								cx = ctx;
								cy = cty;
								cz = ctz;
								ax = atx;
								ay = aty;
								az = atz;
								previousIndex = index;
								x = tx;
								y = ty;
								z = tz;
								cterrain = ctterrain;
								continue outer;									
							}
						} else {
							if( isSolid ) {
								previousConsecutiveAir = 0;
							} else {
								previousConsecutiveAir++;
							}
						}
					} else {
						// TODO : there may be better ways to get this
						previousConsecutiveAir = 0;
					}
				}
			}
			done = true;
			// has naturally ended, we should add on the deltas
			deltaX += (ax - previousTurnX);
			deltaY += (ay - previousTurnY);
			deltaZ += (az - previousTurnZ);			
		}
		deltas.x = deltaX;// >> PRECISION;
		deltas.y = deltaY;// >> PRECISION;
		deltas.z = deltaZ;// >> PRECISION;
		return (primaryIndex+1) | (swappedIndex?SWAPPED_FLAG:0x0);
	}
	
	public static class VerticalEdge {
		protected int cx;
		protected int cy;
		protected int cz;
		protected int x;
		protected int y;
		protected int z;
		
		protected int remainingLength;	

		public String toString() {
			return getClass().getSimpleName() + "{cx="+cx+" cy="+cy+" cz="+cz+" x="+x+" y="+y+" z="+z+" remainingLength="+remainingLength+"}";
		}
	}

}
